package com.google.gwt.mobile.widgets.event;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.Node;
import com.google.gwt.mobile.widgets.utils.Point;
import com.google.gwt.mobile.widgets.utils.Utils;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.ui.RootLayoutPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * The Class TouchController.
 */
public abstract class TouchController implements EventListener {

  private static final double MIN_SPEED = 0.2;
  private static final double MAX_SPEED = 8;

  private final List<DragEventHandler> _dragEventHandlers = new ArrayList<DragEventHandler>();
  private final List<SwipeEventHandler> _swipeEventHandlers = new ArrayList<SwipeEventHandler>();
  protected DragEventHandler _capturingDragEventsHandler = null;
  protected Widget _source;
  private boolean _isDown = false;
  private boolean _suppressNextClick = false;
  private long _lastDragTimeStamp = 0;
  private final Point _lastDragPos = new Point(0, 0);
  private long _currDragTimeStamp = 0;
  private final Point _currDragPos = new Point(0, 0);

  private JavaScriptObject _clickListener;
  protected JavaScriptObject _dragStartListener;
  protected JavaScriptObject _dragMoveListener;
  protected JavaScriptObject _dragEndListener;

  // TODO Use deferred binding result
  protected static TouchController INSTANCE = new TouchControllerMobile();

  TouchController() {
    init();
  }

  /**
   * Gets the.
   * 
   * @return the touch controller
   */
  public static TouchController get() {
    return INSTANCE;
  }

  protected void init() {
    _source = RootLayoutPanel.get();
    registerClickListener();
  }

  /**
   * Adds the drag event handler.
   * 
   * @param dragHandler the drag handler
   */
  public void addDragEventHandler(final DragEventHandler dragHandler) {
    _dragEventHandlers.add(dragHandler);
  }

  /**
   * Adds the swipe event handler.
   * 
   * @param swipeHandler the swipe handler
   */
  public void addSwipeEventHandler(final SwipeEventHandler swipeHandler) {
    _swipeEventHandlers.add(swipeHandler);
  }

  /**
   * Removes the drag event handler.
   * 
   * @param dragHandler the drag handler
   */
  public void removeDragEventHandler(final DragEventHandler dragHandler) {
    _dragEventHandlers.remove(dragHandler);
  }

  /**
   * Removes the swipe event handler.
   * 
   * @param swipeHandler the swipe handler
   */
  public void removeSwipeEventHandler(final SwipeEventHandler swipeHandler) {
    _swipeEventHandlers.remove(swipeHandler);
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * com.google.gwt.user.client.EventListener#onBrowserEvent(com.google.gwt.
   * user.client.Event)
   */
  @Override
  public void onBrowserEvent(final Event e) {
    String type = e.getType();
    if (type.equals("click")) {
      onClick(e);
    }
  }

  private void onClick(final Event e) {
    if (_suppressNextClick) {
      e.stopPropagation();
      _suppressNextClick = false;
    }
  }

  // TODO: May need an onPreStart event to indicate that mouse is down, but no
  // movement yet,
  // so onStart event can actually mean drag is indeed started.
  protected void onStart(final Event e, final Point p) {
    _isDown = true;
    _suppressNextClick = false;
    Date currentDateTime = new Date();
    _lastDragTimeStamp = currentDateTime.getTime();
    _currDragTimeStamp = _lastDragTimeStamp;
    _lastDragPos.clone(p);
    _currDragPos.clone(p);
    DragEvent dragEvent =
        new DragEvent(e, DragEvent.DragType.START, p.X(), p.Y(), p.X() - _currDragPos.X(), p.Y()
            - _currDragPos.Y());
    fireDragEvent(dragEvent);
  }

  protected void onMove(final Event e, final Point p) {
    if (_isDown) {
      if (p.equals(_currDragPos)) {
        return;
      }

      _suppressNextClick = true;

      DragEvent dragEvent =
          new DragEvent(e, DragEvent.DragType.MOVE, p.X(), p.Y(), p.X() - _currDragPos.X(), p.Y()
              - _currDragPos.Y());
      fireDragEvent(dragEvent);

      _lastDragPos.clone(_currDragPos);
      _lastDragTimeStamp = _currDragTimeStamp;
      _currDragPos.clone(p);
      Date currentDateTime = new Date();
      _currDragTimeStamp = currentDateTime.getTime();
    }
  }

  protected void onEnd(final Event e, final Point p) {
    if (_isDown) {
      _isDown = false;
      DragEvent dragEvent =
          new DragEvent(e, DragEvent.DragType.END, p.X(), p.Y(), p.X() - _currDragPos.X(), p.Y()
              - _currDragPos.Y());
      fireDragEvent(dragEvent);

      double distanceX = p.X() - _lastDragPos.X();
      double distanceY = p.Y() - _lastDragPos.Y();
      double distance;
      SwipeEvent.Direction swipeType;

      if (Math.abs(distanceX) > Math.abs(distanceY)) {
        if (distanceX > 0) {
          swipeType = SwipeEvent.Direction.RIGHT;
        } else {
          swipeType = SwipeEvent.Direction.LEFT;
        }
        distance = Math.abs(distanceX);
      } else {
        if (distanceY > 0) {
          swipeType = SwipeEvent.Direction.DOWN;
        } else {
          swipeType = SwipeEvent.Direction.UP;
        }
        distance = Math.abs(distanceY);
      }

      Date currentDateTime = new Date();
      long time = currentDateTime.getTime() - _lastDragTimeStamp;
      double speed = Math.abs(distance / time);

      speed = (speed > MAX_SPEED) ? MAX_SPEED : speed;

      if (speed > MIN_SPEED) {
        SwipeEvent swipeEvent = new SwipeEvent(e, swipeType, speed);
        fireSwipeEvent(swipeEvent);
      }
    }
  }

  protected void fireDragEvent(final DragEvent e) {
    if (_capturingDragEventsHandler != null) {
      e.dispatch(_capturingDragEventsHandler);
      return;
    }

    EventTarget target = e.getNativeEvent().getEventTarget();
    Node node = Node.as(target);

    if (!Element.is(node)) {
      node = node.getParentNode(); // Text node
    }

    if (Element.is(node)) {
      Element el = Element.as(target);
      int count = 0;
      while (el != null) {
        for (DragEventHandler handler : _dragEventHandlers) {
          if (el.equals(handler.getElement())) {
            e.dispatch(handler);
            count++;

            if (e.isStopPropagation() || count == _dragEventHandlers.size()) {
              return;
            }
          }
        }
        el = el.getParentElement();
      }
    }
  }

  protected void fireSwipeEvent(final SwipeEvent e) {
    EventTarget target = e.getNativeEvent().getEventTarget();
    Node node = Node.as(target);

    if (!Element.is(node)) {
      node = node.getParentNode(); // Text node
    }

    if (Element.is(node)) {
      Element el = Element.as(target);
      int count = 0;
      while (el != null) {
        for (SwipeEventHandler handler : _swipeEventHandlers) {
          if (el.equals(handler.getElement())) {
            e.dispatch(handler);
            count++;

            if (e.isStopPropagation() || count == _swipeEventHandlers.size()) {
              return;
            }
          }
        }
        el = el.getParentElement();
      }
    }
  }

  /**
   * Suppress next click.
   */
  public void suppressNextClick() {
    _suppressNextClick = true;
  }

  protected void registerClickListener() {
    if (_clickListener == null) {
      _clickListener = Utils.addEventListener(_source.getElement(), "click", true, this);
    }
  }

  protected void unregisterClickListener() {
    if (_clickListener != null) {
      Utils.removeEventListener(_source.getElement(), "click", true, _clickListener);
      _clickListener = null;
    }
  }

  /**
   * Suspend.
   */
  public void suspend() {
    unregisterClickListener();
  }

  /**
   * Resume.
   */
  public void resume() {
    registerClickListener();
  }

  /**
   * Capture drag events.
   * 
   * @param cachingHandler the caching handler
   * @return true, if successful
   */
  public boolean captureDragEvents(final DragEventHandler cachingHandler) {
    if (_capturingDragEventsHandler != null) {
      return false;
    }
    _capturingDragEventsHandler = cachingHandler;
    return true;
  }

  /**
   * Release capture.
   * 
   * @param cachingHandler the caching handler
   * @return true, if successful
   */
  public boolean releaseCapture(final DragEventHandler cachingHandler) {
    if (_capturingDragEventsHandler == null) {
      return true;
    }
    if (_capturingDragEventsHandler != cachingHandler) {
      return false;
    }
    _capturingDragEventsHandler = null;
    return true;
  }
}
